#!/usr/bin/python2.4
#
# Copyright 2011 Google Inc. All Rights Reserved.

"""KMLProxy provides caching and inline modification of KML files.

This controller owns the /kml/.* namespace for the server, and provides three
interfaces to the world:

  1. Present a KML file to the user that may or may not be already cached.
  2. Update a single KML file from the source, and store in the cache. This
     is usually invoked as a one-off activity.
  3. Update all KML files from their source addresses, and update the cache for
     those requests that succeed. Upon encountering an error, give up for all of
     the KML files.

KML content will be transformed during the update stage, and stored in the
memcache with appropriate changes already made. This means the KML serving does
not have to know about any transformations, and in the event of a broken
controller being uploaded, we will only ever serve material that has
successfully gone through the update phase.

TODO(bst): Make the update all routine not give up for broken KMLs.
"""

__author__ = 'bst@google.com (Ben Stewart)'

import base64
import datetime
import hashlib
import logging
import re
import urllib

import controllers
from controllers import geocode
from django.utils import simplejson, html
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

# Base URL for Mapvisage. May be updated for test versions.
# TODO(bst): Update based on app.yaml, if possible.
MAPVISAGE_BASE = 'http://mapvisage.appspot.com/'

# Default icon from a Fusion Tables export
DEFAULT_ICON = 'http://maps.google.com/mapfiles/kml/paddle/red-blank_maps.png'

AUTH_HASH = '4ef667d4c8fb1056f07b9ae6f5061cd4'

# Search and Replace fragment for a road closure KML.
ROAD_CLOSURE_ICON = (DEFAULT_ICON,
                     MAPVISAGE_BASE + 'static/floodmap/images/roadclosure.png')

# Search and Replace fragment for a photo KML.
PHOTO_ICON = (DEFAULT_ICON,
              MAPVISAGE_BASE + 'static/floodmap/images/photo.png')

# Search and Replace fragment for a road closure KML.
EARTHQUAKE_ICON = (DEFAULT_ICON,
                   'http://mapvisage.appspot.com/static/images/earthquake.png')
                   # MAPVISAGE_BASE + 'static/images/earthquake.png')

NEWS_ICON = ('http://maps.gstatic.com/intl/en_au/mapfiles/ms/micons/' +
             'blue-dot.png',
             'http://mapvisage.appspot.com/static/images/report.png')

NEWS_ICON_2 = ('http://maps.gstatic.com/intl/en_nz/mapfiles/ms/micons/' +
               'blue.png',
               'http://mapvisage.appspot.com/static/images/report.png')

SHELTER_ICON = ('http://maps.gstatic.com/intl/en_nz/mapfiles/ms/micons/' +
                'yellow.png',
                'http://mapvisage.appspot.com/static/images/shelter.png')

CLOSED_ICON = ('http://maps.gstatic.com/intl/en_nz/mapfiles/ms/micons/' +
               'red.png',
               'http://mapvisage.appspot.com/static/floodmap/images/caution-triangle.png')

# Remove the extraneous <Folder> around Fusion Table output that Maps
# chokes on - no custom images are parsed if the Folder is left in place.
REMOVE_FT_FOLDER = [('<Folder id="Fusiontables">', ''),
                    ('<name>Fusiontables folder</name>', ''),
                    ('</Folder>', '')]

# #*(&(*$&#(*$#&(#*&(#$ 
def FixFtKmlLocation(kml):
  fixup = re.compile(r'<address>([-\.\d]*),([-\.\d]*)</address>')
  return fixup.sub(r'<Point><coordinates>\2,\1,0</coordinates></Point>', kml)

# Dictionary of configured KML files.
#
# This is a dictionary of dictionaries, where the top-level key is the name of
# the KML file, and is matched with the second path fragment (the bit after
# /kml/ and before the optional /__update).
#
# Each KML file must consist of a dictionary with at least two keys:
#
# - url: Source URL of the KML file. We do not care about the returned
#        content type or content disposition.
#
# - icons: This is an array of search-and-replace 2-tuples, consisting of
#          (source text, replacement text).
#
CONFIG = {
    'vicevac': {
        'url': ('http://maps.google.com.au/maps/ms?hl=en&ie=UTF8&source=' +
                'embed&oe=UTF8&msa=0&msid=216570432711189139958.000499c1' +
                'a23499b9afcb1&output=kml'),
        'icons': [
            ('http://maps.gstatic.com/intl/en_au/mapfiles/ms/micons/' +
             'blue-dot.png',
             'http://mapvisage.appspot.com/static/floodmap/images/' +
             'rivers/evaccentre.png'),
            ('http://maps.gstatic.com/intl/en_ALL/mapfiles/ms/micons/' +
             'blue-dot.png',
             'http://mapvisage.appspot.com/static/floodmap/images/' +
             'rivers/evaccentre.png')
            ]
        },
    'vicelec': {
        'url': ('http://www.google.com/fusiontables/exporttable?query=' +
                'select+col0%2C+col1%2C+col6%2C+col7%2C+col2%2C+col5%2C+' +
                'col4%2C+col3+from+395303+&o=kmllink&g=col0'),
        'icons': [
            ('http://maps.google.com/mapfiles/kml/paddle/red-blank_maps.png',
             'http://mapvisage.appspot.com/static/floodmap/images/' +
             'power-outage.png'),
            ] + REMOVE_FT_FOLDER,
        },
    'vicses': {
        'url': ('http://www.google.com/fusiontables/exporttable?query=select'
                '+col0%2C+col1%2C+col2%2C+col4+from+395832+&o=kmllink&g=col0'),
        'icons': [
            ('http://maps.google.com/mapfiles/kml/paddle/red-blank_maps.png',
             'http://mapvisage.appspot.com/static/floodmap/images/' +
             'caution-triangle.png'),
            ('<Folder id="Fusiontables">', ''),
            ('<name>Fusiontables folder</name>', ''),
            ('</Folder>', ''),
            ]
        },
    'qldroadclosures': {
        'url': ('http://www.google.com/fusiontables/exporttable?query=' +
                'select+col0%2C+col1%2C+col2%2C+col3%2C+col4+from+393082+' +
                '&o=kmllink&g=col0'),
        'icons': [
            ROAD_CLOSURE_ICON,
            ('<Folder id="Fusiontables">', ''),
            ('<name>Fusiontables folder</name>', ''),
            ('</Folder>', '')],
        },
    'vicroadclosures': {
        'url': ('http://www.google.com/fusiontables/exporttable?query=' +
                'select+col0%2C+col1%2C+col2%2C+col3%2C+col4+from+396048+' +
                '&o=kmllink&g=col0'),
        'icons': [
            ROAD_CLOSURE_ICON,
            ('<Folder id="Fusiontables">', ''),
            ('<name>Fusiontables folder</name>', ''),
            ('</Folder>', '')],
        },
    'nswses': {
        'url': ( 'http://www.google.com/fusiontables/exporttable?query=select'
                  '+col0%2C+col1%2C+col2%2C+col3+from+395677+&o=kmllink&g=col0'
               ),
        'icons': [
            ('http://maps.google.com/mapfiles/kml/paddle/red-blank_maps.png',
             'http://mapvisage.appspot.com/static/floodmap/images/' +
             'caution-triangle.png'),
            ('<Folder id="Fusiontables">', ''),
            ('<name>Fusiontables folder</name>', ''),
            ('</Folder>', ''),
            ]
        },
    'nswroadclosures': {
        'url': ('http://www.google.com/fusiontables/exporttable?query=' +
                'select+col0%2C+col9%2C+col1%2C+col2%2C+col3%2C+col5%2C+col6%2C+col7%2C+col8+from+401993+' +
                '&o=kmllink&g=col0'),
        'icons': [
            ROAD_CLOSURE_ICON,
            ] + REMOVE_FT_FOLDER,
        'fixes': [ FixFtKmlLocation ]
        },
    'clouds': {
        'url': ('http://mapvisage.appspot.com/clouds/qld.kml'),
        'icons': []
        },
    'qldwind': {
        'url': ('http://www.google.com/fusiontables/exporttable?query=' +
                'select+col0%2C+col1%2C+col2%2C+col3%2C+col4%2C+col5%2C+' +
                'col6%2C+col7+from+423240+&o=kmllink&g=col1'),
        'icons': REMOVE_FT_FOLDER
        },
    'christchurch1': {
        'url': ('http://maps.google.com/maps/ms?msa=0&msid=' +
                '207970900160165406611.00049cd6e5b046bacf23b&output=kml'),
        'icons': [NEWS_ICON]
        },
    'christchurch2': {
        'url': ('http://maps.google.com/maps/ms?msa=0&msid=' +
                '205972314722231895961.00049cd3bc095df8c4355&output=kml'),
        'icons': [NEWS_ICON_2, SHELTER_ICON, CLOSED_ICON]
        },
    'christchurchdamage': {
        'url': ('http://maps.google.com/maps/ms?ie=UTF8&msa=0&output=nl&msid=' +
                '205430068818878278687.00049cd5ad806c11ef1b6'),
        'icons': []
        },
    }


def loadMap(mapId):
  """Load a KML file from CONFIG, and return the transformed content.

  This routine will fetch the KML file from the internet, and return the page
  content with all defined search-and-replace actions completed.

  Args:
    mapId:  Key from the CONFIG dictionary to fetch.

  Returns:
    Page content with all search-and-replaces completed. Should be KML.
  """

  if mapId in CONFIG:
    url = CONFIG[mapId]['url']

    raw = urllib.urlopen(url)
    return loadMapFromFile(mapId, raw)

def loadMapFromFile(mapId, f):
  icons = CONFIG[mapId]['icons']
  page = f.read()

  for icon in icons:
    page = page.replace(icon[0], icon[1])
  for fixup in CONFIG[mapId].get('fixes', []):
    page = fixup(page)

  return page


def updateAllMaps():
  """Update the cache for all defined KML files in CONFIG."""
  from google.appengine.api import memcache

  for mapId in CONFIG:
    data = loadMap(mapId)
    if data is not None and len(data) > 0:
      memcache.set('kmlproxy_' + mapId, data)


def updateMap(mapId):
  """Update the cache for a single KML file in CONFIG.

  Args:
    mapId:  Key from the CONFIG dictionary to update.
  """
  from google.appengine.api import memcache
  data = loadMap(mapId)
  if data is not None and len(data) > 0:
    memcache.set('kmlproxy_' + mapId, data)


def uploadMap(mapId, f):
  from google.appengine.api import memcache

  data = loadMapFromFile(mapId, f)
  if data is not None and len(data) > 0:
    memcache.set('kmlproxy_' + mapId, data)


def getMap(mapId):
  """Retrieve a KML from the cache, or from the net if it isn't cached.

  Args:
    mapId:  Map ID; key in the CONFIG dictionary.
  """
  from google.appengine.api import memcache
  kml = memcache.get('kmlproxy_' + mapId)
  if kml is None:
    updateMap(mapId)
    kml = memcache.get('kmlproxy_' + mapId)

  return kml


class UpdatePage(webapp.RequestHandler):
  """Request handler for a manual update of a map."""

  def get(self):
    """Serve a request to update a KML file.

    Extract the KML name from the URL path, hand it off to updateMap() and
    display OK on completion.
    """

    urlParts = self.request.path.split('/')
    mapId = ''
    if len(urlParts) >= 3:
      mapId = urlParts[2]
    updateMap(mapId)
    self.response.headers['Content-Type'] = 'text/plain'
    self.response.out.write('OK')


class UploadPage(webapp.RequestHandler):
  """Request handler to explicitly upload a KML file."""

  def put(self):
    authed = False

    if 'Authorization' in self.request.headers:
      a = self.request.headers['Authorization'].split(' ')
      if len(a) == 2:
        data = base64.b64decode(a[1])
        password = data.split(':')[1]
        h = hashlib.new('md5')
        h.update(password)

        if AUTH_HASH == h.hexdigest():
          authed = True

    if not authed:
      self.response.set_status(401)
      self.response.headers['WWW-Authenticate'] = 'Basic realm="Mapvisage"'
      self.response.out.write('No access. Please authenticate.')
    else:
      urlParts = self.request.path.split('/')
      mapId = ''
      if len(urlParts) >= 3:
        mapId = urlParts[2]
      uploadMap(mapId, self.request.body_file)
      self.response.headers['Content-Type'] = 'text/plain'
      self.response.out.write('OK')


class ShowPage(webapp.RequestHandler):
  """Request handler to display a fetched KML file."""

  def get(self):
    """Serve a request to display a KML file.

    Extract the KML name from the URL path, grab the content from getMap(), and
    serve it up with a 5 minute cache time.
    """

    urlParts = self.request.path.split('/')
    mapId = ''
    if len(urlParts) >= 3:
      mapId = urlParts[2]

    self.response.headers['Content-Type'] = 'application/vnd.google-earth.kml+xml'
    controllers.cached_response(self.response, duration=300)
    self.response.out.write(getMap(mapId))


class UpdateAllPage(webapp.RequestHandler):
  """Request handler to update all KML files' caches."""

  def get(self):
    """Serve a request to update all KML file caches.

    This routine asks for all maps to be updated, then displays OK on
    completion. Please note that where the content is being fetched from a
    particularly distant or useless webserver, or if there is a large number of
    maps to be updated, this routine may time out before completion.

    This routine would ideally be triggered from a cron task, which permits
    requests to run for longer than the standard 30 seconds under AppEngine,
    extending it to 10 minutes.
    """

    updateAllMaps()
    self.response.headers['Content-Type'] = 'text/plain'
    self.response.out.write('OK')


# Automatically define a set of URL handlers based upon CONFIG. Both
# /kml/mapname and /kml/mapname/update__ will be provided for each map, in
# addition to the standard /kml/update__.
urlHandlers = (map(lambda mapId: ('/kml/%s' % mapId, ShowPage), CONFIG) +
               map(lambda mapId: ('/kml/%s/update__' % mapId, UpdatePage),
                   CONFIG) +
               map(lambda mapId: ('/kml/%s/upload__' % mapId, UploadPage),
                   CONFIG) +
               [('/kml/update__', UpdateAllPage)])

application = webapp.WSGIApplication(urlHandlers, debug=True)


def main():
  run_wsgi_app(application)

if __name__ == "__main__":
  main()
